﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using ActionProviderImplementation;
using System.Data.Entity;
using System.Threading;
using System.Data.Services;
using System.Data.Services.Providers;

namespace MoviesWebsite.Models
{
    public class MoviesModel: DbContext
    {
        #region Initializers
        static MoviesModel()
        {
            Database.SetInitializer(new MoviesModelInitializer());
        }
        protected override void  OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Movie>().HasMany(m => m.Tags).WithMany();
            modelBuilder.ComplexType<NameValuePair>();
 	        base.OnModelCreating(modelBuilder);
        }
        #endregion

        #region Sets
        public DbSet<Actor> Actors { get; set; }
        public DbSet<Director> Directors { get; set; }
        public DbSet<Genre> Genres { get; set; }
        public DbSet<Movie> Movies { get; set; }
        public DbSet<UserRental> Rentals { get; set; }
        public DbSet<Tag> Tags { get; set; }
        public DbSet<User> Users { get; set; }
        public DbSet<UserRating> Ratings { get; set; }
        #endregion

        #region OccasionallyBindableActions (i.e. can be bound only when binding parameter is in appropriate state)
        /// <summary>
        ///  Allows you to checkout a movie, only advertized if the movie is NOT already checked out
        /// </summary>
        /// <param name="movie"></param>
        [OccasionallyBindableAction("CanCheckout")]
        public void Checkout(Movie movie) 
        {
            var userName = GetUsername();
            var movieID = movie.ID;

            var alreadyCheckedOut = this.Rentals.Any(r => 
                r.UserUsername == userName && 
                r.MovieID == movieID && 
                r.CheckedIn == null
            );

            if (!alreadyCheckedOut)
            {
                // add a new rental
                this.Rentals.Add(
                    new UserRental { 
                        MovieID = movieID, 
                        CheckedOut = DateTime.Now, 
                        UserUsername = userName
                    }
                );
            }
        }
        [SkipCheckForFeeds]
        public bool CanCheckout(Movie movie)
        {
            var username = GetUsername();
            var rental = Rentals.SingleOrDefault(r => r.UserUsername == username && r.MovieID == movie.ID && r.CheckedIn == null);
            if (rental == null) return true;
            else if (rental.CheckedIn.HasValue) return true;
            else return false;
        }
        /// <summary>
        /// Allows you to checkin a movie, only advertized if the movie is checked out.
        /// </summary>
        /// <param name="movie"></param>
        [OccasionallyBindableAction("CanCheckin")]
        public void Checkin(Movie movie) 
        {
        var userName = GetUsername();
        var movieID = movie.ID;

        var existingRental = this.Rentals.SingleOrDefault(r => 
            r.UserUsername == userName &&
            r.MovieID == movieID &&
            r.CheckedIn == null
        );

        if (existingRental != null)
        {
            existingRental.CheckedIn = DateTime.Now;
        }

        }
        [SkipCheckForFeeds]
        public bool CanCheckin(Movie movie)
        {
            return !CanCheckout(movie);
        }
        /// <summary>
        /// This action allows you to tag a movie, 
        /// but is only advertized if you are an administrator.
        /// However people might guess this URL so when the action is invoked we CHECK too.
        /// </summary>
        [OccasionallyBindableAction("CanSetTag")]
        public void SetTag(Movie movie, string tagName)
        {
            var movieID = movie.ID;

            if (!GetIsAdministrator())
                throw new DataServiceException(401, "Access Denied");

            if (!Movies.Any(m => m.ID == movieID && m.Tags.Any(t => t.Value == tagName)))
            {
                var tag = Tags.SingleOrDefault(t => t.Value == tagName);
                if (tag == null) throw new DataServiceException(404, "Tag Not Found");
                movie.Tags.Add(tag);
            }
        }
        /// <summary>
        /// Not while it requires a database query to check if a user is an admin
        /// it can be cached per request, so even if called for every item in a feed
        /// the cost after the first check is negligable
        /// </summary>
        public bool CanSetTag(Movie movie) {
            return GetIsAdministrator();
        }
        #endregion

        #region AlwaysBindableActions (i.e. can be bound independent of binding parameter state)
        /// <summary>
        /// This allows you to rate a movie,
        /// this is always available - but if you call this twice for the same movie
        /// your existing rating will be updated
        /// </summary>
        [Action]
        public void Rate(Movie movie, int rating) 
        {
            var userName = GetUsername();
            var movieID = movie.ID;

            var existingRating = this.Ratings.SingleOrDefault(r => 
                r.UserUsername == userName && 
                r.MovieID == movieID
            );

            if (existingRating == null)
            {
                this.Ratings.Add(new UserRating { MovieID = movieID, Rating = rating, UserUsername = userName });
            }
            else 
            {
                existingRating.Rating = rating;
            }
        }
        [Action]
        public void RateAll(IQueryable<Movie> movies, IEnumerable<int> ratings)
        {
            var count = movies.Count();
            if (count != ratings.Count()) throw new Exception("Number of ratings must match number of movies");
            
            var movieRatings = movies.AsEnumerable().Zip(ratings, (m, r) => new { Movie = m, Rating = r });
            foreach (var movieRating in movieRatings)
            {
                Rate(movieRating.Movie, movieRating.Rating);
            }
        }
        [Action]
        public bool CheckinAll(IQueryable<Movie> movies)
        {
            var username = GetUsername();
            var rentals = Rentals.Where(r => movies.Contains(r.Movie))
                                 .Where(r => r.UserUsername == username && r.CheckedIn == null);
            
            if (rentals.Count() > 20) throw new Exception("Can only checkin a maximum of 20 movies at one time.");

            var now = DateTime.UtcNow;
            foreach (var rental in rentals)
            {
                rental.CheckedIn = now; 
            }
            return true;
        }
        [Action]
        public bool SetGenres(IQueryable<Movie> movies, IEnumerable<string> genres)
        {
            var count = movies.Count();
            if (count != genres.Count()) throw new Exception("Number of genres MUST match number of movies");

            var movieGenres = movies.AsEnumerable().Zip(genres, (m, g) => new { Movie = m, Genre = g });

            foreach (var movieGenre in movieGenres)
                SetGenre(movieGenre.Movie, movieGenre.Genre);
            return true;
        }
        [Action]
        public bool SetGenre(Movie movie, string genre)
        {
            var genreEnt = Genres.Single(g => g.Name == genre);
            movie.Genre = genreEnt;
            return true;
        }
        [Action]
        public bool CheckoutAll(IQueryable<Movie> movies)
        {
            if (movies.Count() > 20) throw new Exception("Can only checkout a maximum of 20 movies at one time.");
            foreach (var movie in movies)
            {
                Checkout(movie);
            }
            return true;
        }    
        #endregion

        #region NonBindableActions (i.e. supercharged ServiceOperations)
        [NonBindableAction]
        public Movie CreateMovie(string name, string genre)
        { 
            var genreEntity = Genres.Single(g => g.Name == genre);
            var movie = new Movie { Name = name, Genre = genreEntity };
            Movies.Add(movie);
            
            // NOTE: SaveChanges is NOT required here (indeed it would be dangerous), 
            // Because WCF DS calls SaveChanges() after invoking this method but before trying to serialize the movie
            // So even though any Identity Columns are not set when we return the movie below
            // they are set by the time the object is serialized back to the client...
            return movie;
        }
        #endregion

        #region Utility Functions
        private string GetUsername()
        {
            // Note: for testing purposes this is using a Authentication/TestingModule
            //       to establish an identity that matches the sample data...
            return HttpContext.Current.User.Identity.Name;
        }
        private User GetUser()
        {
            var username = GetUsername();
            if (_user == null)
                _user = Users.SingleOrDefault(u => u.Username == username);
            return _user;
        }
        private bool GetIsAdministrator()
        {
            var user = GetUser();
            return user != null && user.IsAdministrator;
        }
        User _user;
        #endregion
    }
}